/*
 * Copyright 2014 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.maps.android.heatmaps;

import android.graphics.Color;

import java.util.HashMap;

/**
 * A class to generate a color map from a given array of colors and the fractions
 * that the colors represent by interpolating between their HSV values.
 * This color map is to be used in the HeatmapTileProvider.
 */
public class Gradient {

    private class ColorInterval {
        private final int color1;
        private final int color2;

        /**
         * The period over which the color changes from color1 to color2.
         * This is given as the number of elements it represents in the colorMap.
         */
        private final float duration;

        private ColorInterval(int color1, int color2, float duration) {
            this.color1 = color1;
            this.color2 = color2;
            this.duration = duration;
        }
    }

    private static final int DEFAULT_COLOR_MAP_SIZE = 1000;

    /**
     * Size of a color map for the heatmap
     */
    public final int mColorMapSize;

    /**
     * The colors to be used in the gradient
     */
    public int[] mColors;

    /**
     * The starting point for each color, given as a percentage of the maximum intensity
     */
    public float[] mStartPoints;

    /**
     * Creates a Gradient with the given colors and starting points.
     * These are given as parallel arrays.
     *
     * @param colors      The colors to be used in the gradient
     * @param startPoints The starting point for each color, given as a percentage of the maximum intensity
     *                    This is given as an array of floats with values in the interval [0,1]
     */
    public Gradient(int[] colors, float[] startPoints) {
        this(colors, startPoints, DEFAULT_COLOR_MAP_SIZE);
    }

    /**
     * Creates a Gradient with the given colors and starting points which creates a colorMap of given size.
     * The colors and starting points are given as parallel arrays.
     *
     * @param colors       The colors to be used in the gradient
     * @param startPoints  The starting point for each color, given as a percentage of the maximum intensity
     *                     This is given as an array of floats with values in the interval [0,1]
     * @param colorMapSize The size of the colorMap to be generated by the Gradient
     */
    public Gradient(int[] colors, float[] startPoints, int colorMapSize) {
        if (colors.length != startPoints.length) {
            throw new IllegalArgumentException("colors and startPoints should be same length");
        } else if (colors.length == 0) {
            throw new IllegalArgumentException("No colors have been defined");
        }
        for (int i = 1; i < startPoints.length; i++) {
            if (startPoints[i] <= startPoints[i - 1]) {
                throw new IllegalArgumentException("startPoints should be in increasing order");
            }
        }
        mColorMapSize = colorMapSize;
        mColors = new int[colors.length];
        mStartPoints = new float[startPoints.length];
        System.arraycopy(colors, 0, mColors, 0, colors.length);
        System.arraycopy(startPoints, 0, mStartPoints, 0, startPoints.length);
    }

    private HashMap<Integer, ColorInterval> generateColorIntervals() {
        HashMap<Integer, ColorInterval> colorIntervals = new HashMap<Integer, ColorInterval>();
        // Create first color if not already created
        // The initial color is transparent by default
        if (mStartPoints[0] != 0) {
            int initialColor = Color.argb(
                    0, Color.red(mColors[0]), Color.green(mColors[0]), Color.blue(mColors[0]));
            colorIntervals.put(0, new ColorInterval(initialColor, mColors[0], mColorMapSize * mStartPoints[0]));
        }
        // Generate color intervals
        for (int i = 1; i < mColors.length; i++) {
            colorIntervals.put(((int) (mColorMapSize * mStartPoints[i - 1])),
                new ColorInterval(mColors[i - 1], mColors[i],
                    (mColorMapSize * (mStartPoints[i] - mStartPoints[i - 1]))));
        }
        // Extend to a final color
        // If color for 100% intensity is not given, the color of highest intensity is used.
        if (mStartPoints[mStartPoints.length - 1] != 1) {
            int i = mStartPoints.length - 1;
            colorIntervals.put(((int) (mColorMapSize * mStartPoints[i])),
                    new ColorInterval(mColors[i], mColors[i], mColorMapSize * (1 - mStartPoints[i])));
        }
        return colorIntervals;
    }

    /**
     * Generates the color map to use with a provided gradient.
     *
     * @param opacity Overall opacity of entire image: every individual alpha value will be
     *                multiplied by this opacity.
     * @return the generated color map based on the gradient
     */
    int[] generateColorMap(double opacity) {
        HashMap<Integer, ColorInterval> colorIntervals = generateColorIntervals();
        int[] colorMap = new int[mColorMapSize];
        ColorInterval interval = colorIntervals.get(0);
        int start = 0;
        for (int i = 0; i < mColorMapSize; i++) {
            if (colorIntervals.containsKey(i)) {
                interval = colorIntervals.get(i);
                start = i;
            }
            float ratio = (i - start) / interval.duration;
            colorMap[i] = interpolateColor(interval.color1, interval.color2, ratio);
        }
        if (opacity != 1) {
            for (int i = 0; i < mColorMapSize; i++) {
                int c = colorMap[i];
                colorMap[i] = Color.argb((int) (Color.alpha(c) * opacity),
                        Color.red(c), Color.green(c), Color.blue(c));
            }
        }

        return colorMap;
    }

    /**
     * Helper function for creation of color map
     * Interpolates between two given colors using their HSV values.
     *
     * @param color1 First color
     * @param color2 Second color
     * @param ratio  Between 0 to 1. Fraction of the distance between color1 and color2
     * @return Color associated with x2
     */
    static int interpolateColor(int color1, int color2, float ratio) {

        int alpha = (int) ((Color.alpha(color2) - Color.alpha(color1)) * ratio + Color.alpha(color1));

        float[] hsv1 = new float[3];
        Color.RGBToHSV(Color.red(color1), Color.green(color1), Color.blue(color1), hsv1);
        float[] hsv2 = new float[3];
        Color.RGBToHSV(Color.red(color2), Color.green(color2), Color.blue(color2), hsv2);

        // adjust so that the shortest path on the color wheel will be taken
        if (hsv1[0] - hsv2[0] > 180) {
            hsv2[0] += 360;
        } else if (hsv2[0] - hsv1[0] > 180) {
            hsv1[0] += 360;
        }

        // Interpolate using calculated ratio
        float[] result = new float[3];
        for (int i = 0; i < 3; i++) {
            result[i] = (hsv2[i] - hsv1[i]) * (ratio) + hsv1[i];
        }

        return Color.HSVToColor(alpha, result);
    }

}
